﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.NRefactory.CSharp;
using ICSharpCode.NRefactory.CSharp.Completion;
using ICSharpCode.NRefactory.Documentation;
using ICSharpCode.NRefactory.Editor;
using ICSharpCode.NRefactory.TypeSystem;
using Peter.Common.Code;

namespace Peter.Net.CodeCompletion
{
   /// <summary>
   /// Code completion for C# code.
   /// </summary>
   [Export (typeof (BaseCodeCompletion))]
   public class CSharpCodeCompletion : BaseCodeCompletion
   {
      /// <summary>
      /// Initializes a new C# code completion object.
      /// </summary>
      public CSharpCodeCompletion ()
      {
         this.ProjectContent = new CSharpProjectContent ();
         var assemblies = new List <Assembly>
         {
            typeof (object).Assembly, // mscorlib
            typeof (Uri).Assembly, // System.dll
            typeof (Enumerable).Assembly, // System.Core.dll
         };

         var unresolvedAssemblies = new IUnresolvedAssembly[assemblies.Count];
         Parallel.For (0, assemblies.Count,
            delegate (int i)
            {
               var loader = new CecilLoader ();
               var path = assemblies [i].Location;
               loader.DocumentationProvider = GetXmlDocumentation (path);
               unresolvedAssemblies[i] = loader.LoadAssemblyFile (path);
            });
         this.ProjectContent = this.ProjectContent.AddAssemblyReferences ((IEnumerable<IUnresolvedAssembly>)unresolvedAssemblies);
      }

      /// <summary>
      /// Updates the file in the code completion code base.
      /// </summary>
      /// <param name="pathToFile">Full path to file to update.</param>
      public override async void UpdateFile (string pathToFile)
      {
         var fileData = System.IO.File.ReadAllText (pathToFile);
         var syntaxTree = await Task.Run (() => new CSharpParser ().Parse (fileData, pathToFile));
         syntaxTree.Freeze ();
         this.ProjectContent = this.ProjectContent.AddOrUpdateFiles (syntaxTree.ToTypeSystem ());
      }

      /// <summary>
      /// Gets the xml documentation for the given dll.
      /// </summary>
      /// <param name="dllPath">Path to dll to get xml documentation for.</param>
      /// <returns>XmlDocumentationProvider</returns>
      protected override XmlDocumentationProvider GetXmlDocumentation (string dllPath)
      {
         if (string.IsNullOrEmpty (dllPath))
            return null;

         var xmlFileName = Path.GetFileNameWithoutExtension (dllPath) + ".xml";
         var dirName = Path.GetDirectoryName (dllPath);
         var localPath = Path.Combine (dirName ?? string.Empty, xmlFileName);
         if (File.Exists (localPath))
            return new XmlDocumentationProvider (localPath);

         //if it's a .NET framework assembly it's in one of following folders
         var netPath =
            Path.Combine (@"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0",
               xmlFileName);
         if (File.Exists (netPath))
            return new XmlDocumentationProvider (netPath);

         return null;
      }

      /// <summary>
      /// Gets the completion results for the given document.
      /// </summary>
      /// <param name="document">The document to get completion Results for.</param>
      /// <param name="offset">The offset of the caret.</param>
      /// <returns>The code completion result.</returns>
      public async Task<CodeCompletionResult> GetCompletions (IDocument document, int offset)
      {
         return await GetCompletions (document, offset, false);
      }

      /// <summary>
      /// Gets the completion results for the given document.
      /// </summary>
      /// <param name="document">The document to get completion Results for.</param>
      /// <param name="offset">The offset of the caret.</param>
      /// <param name="controlSpace">True if completion is requested via Ctrl+Space, otherwise false.</param>
      /// <returns>The code completion result.</returns>
      public override async Task<CodeCompletionResult> GetCompletions (IDocument document, int offset, bool controlSpace)
      {
         string usings = null;
         //if (ScriptProvider != null)
         //   usings = ScriptProvider.GetUsing ();
         return await GetCompletions (document, offset, controlSpace, usings);
      }

      /// <summary>
      /// Gets the completion results for the given document.
      /// </summary>
      /// <param name="document">The document to get completion Results for.</param>
      /// <param name="offset">The offset of the caret.</param>
      /// <param name="controlSpace">True if completion is requested via Ctrl+Space, otherwise false.</param>
      /// <param name="usings">Any using to use for completion.</param>
      /// <returns>The code completion result.</returns>
      public async Task<CodeCompletionResult> GetCompletions (IDocument document, int offset, bool controlSpace, string usings)
      {
         var result = new CodeCompletionResult ();

         if (String.IsNullOrEmpty (document.FileName))
            return result;

         var completionContext = new CSharpCompletionContext (document, offset, this.ProjectContent, usings);

         var completionFactory = new CSharpCompletionDataFactory (completionContext.TypeResolveContextAtCaret, completionContext);
         var cce = new CSharpCompletionEngine (
             completionContext.Document,
             completionContext.CompletionContextProvider,
             completionFactory,
             completionContext.ProjectContent,
             completionContext.TypeResolveContextAtCaret
             ) { EolMarker = Environment.NewLine, FormattingPolicy = FormattingOptionsFactory.CreateSharpDevelop () };


         var completionChar = completionContext.Document.GetCharAt (completionContext.Offset - 1);
         int startPos, triggerWordLength;
         IEnumerable<ICSharpCode.NRefactory.Completion.ICompletionData> completionData;
         if (controlSpace)
         {
            if (!cce.TryGetCompletionWord (completionContext.Offset, out startPos, out triggerWordLength))
            {
               startPos = completionContext.Offset;
               triggerWordLength = 0;
            }
            completionData = cce.GetCompletionData (startPos, true);
            //this outputs tons of available entities
            //if (triggerWordLength == 0)
            //    completionData = completionData.Concat(cce.GetImportCompletionData(startPos));
         }
         else
         {
            startPos = completionContext.Offset;

            if (char.IsLetterOrDigit (completionChar) || completionChar == '_')
            {
               if (startPos > 1 && char.IsLetterOrDigit (completionContext.Document.GetCharAt (startPos - 2)))
                  return result;
               completionData = cce.GetCompletionData (startPos, false);
               startPos--;
               triggerWordLength = 1;
            }
            else
            {
               completionData = cce.GetCompletionData (startPos, false);
               triggerWordLength = 0;
            }
         }

         result.TriggerWordLength = triggerWordLength;
         result.TriggerWord = completionContext.Document.GetText (completionContext.Offset - triggerWordLength, triggerWordLength);
         
         //cast to AvalonEdit completion data and add to results
         foreach (var completion in completionData)
         {
            var cshellCompletionData = completion as CompletionData;
            if (cshellCompletionData != null)
            {
               cshellCompletionData.TriggerWord = result.TriggerWord;
               cshellCompletionData.TriggerWordLength = result.TriggerWordLength;
               result.CompletionData.Add (cshellCompletionData);
            }
         }

         //method completions
         if (!controlSpace)
         {
            // Method Insight
            var pce = new CSharpParameterCompletionEngine (
                completionContext.Document,
                completionContext.CompletionContextProvider,
                completionFactory,
                completionContext.ProjectContent,
                completionContext.TypeResolveContextAtCaret
            );

            var parameterDataProvider = pce.GetParameterDataProvider (completionContext.Offset, completionChar);
            result.OverloadProvider = parameterDataProvider as IOverloadProvider;
         }

         return result;
      }

      /// <summary>
      /// Attempts to show the code completion window.
      /// </summary>
      /// <param name="enteredText">The text that was entered.</param>
      /// <param name="completionWindow">The completion window from avalon edit.</param>
      /// <param name="insightWindow">The insight window from avalon edit.</param>
      /// <param name="editor">The text editor.</param>
      public override async Task ShowCompletion (string enteredText, CompletionWindow completionWindow, OverloadInsightWindow insightWindow, TextEditor editor)
      {
         if (completionWindow == null)
         {
            int offset;
            var doc = GetCompletionDocument (editor, out offset);
            var results = await this.GetCompletions (doc, offset, enteredText == null);
            if (results == null)
               return;
            if (insightWindow == null && results.OverloadProvider != null)
            {
               insightWindow = new OverloadInsightWindow (editor.TextArea) { Provider = results.OverloadProvider };
               insightWindow.Show ();
               insightWindow.Closed += (sender, args) => insightWindow = null;
            }

            if (results.CompletionData.Any ())
            {
               completionWindow = new CompletionWindow (editor.TextArea) { CloseWhenCaretAtBeginning = enteredText == null };
               completionWindow.StartOffset -= results.TriggerWordLength;

               var data = completionWindow.CompletionList.CompletionData;
               foreach (var completion in results.CompletionData.OrderBy (item => item.Text))
                  data.Add (completion);
               if (results.TriggerWordLength > 0)
                  completionWindow.CompletionList.SelectItem (results.TriggerWord);
               completionWindow.Show ();
               completionWindow.Closed += (sender, args) => completionWindow = null;
            }
         }

         if (!string.IsNullOrEmpty (enteredText) && insightWindow != null)
         {
            var provider = insightWindow.Provider as CSharpOverloadProvider;
            if (provider != null)
            {
               //since the text has not been added yet we need to treat it as if the char has already been inserted
               int offset;
               var doc = GetCompletionDocument (editor, out offset);
               provider.Update (doc, offset);
               //if the windows is requested to be closed we do it here
               if (provider.RequestClose)
               {
                  insightWindow.Close ();
                  insightWindow = null;
               }
            }
         }
      }

      private static IDocument GetCompletionDocument (TextEditor editor, out int offset)
      {
         offset = editor.CaretOffset;
         return editor.Document;
      }

      /// <summary>
      /// Checks if the given extension is supported by this completion item.
      /// </summary>
      /// <param name="extension">Extension to check.</param>
      /// <returns>True if supported otherwise false.</returns>
      public override bool Supports (string extension)
      {
         return extension.ToLower () == ".cs";
      }
   }
}